/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.neuronbit.businessflow.intellij.dialog;

import com.neuronbit.businessflow.intellij.util.GsonUtils;
import com.neuronbit.businessflow.intellij.util.PsiUtils;
import com.intellij.icons.AllIcons;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.util.TreeChooser;
import com.intellij.ide.util.gotoByName.ChooseByNameModel;
import com.intellij.ide.util.gotoByName.ChooseByNamePanel;
import com.intellij.ide.util.gotoByName.ChooseByNamePopupComponent;
import com.intellij.ide.util.gotoByName.GotoClassModel2;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.PsiShortNamesCache;
import com.intellij.psi.util.PsiTypesUtil;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBList;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.indexing.DumbModeAccessType;
import com.intellij.util.indexing.FindSymbolParameters;
import com.intellij.util.ui.JBEmptyBorder;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.util.List;
import java.util.*;

/**
 * This is dialog box for selecting methods of classes
 */
public class PsiMethodDialog extends DialogWrapper {
    private JPanel panel = new JPanel(new BorderLayout());
    private final Project project;
    private final PsiClass psiClass;
    private final String returnTypeFilter;
    private ChooseByNamePanel myGotoByNamePanel;
    private PsiMethod myInitialClass;
    private PsiMethod selectMethod;
    private final GlobalSearchScope myScope;
    private final @NotNull TreeChooser.Filter<PsiMethod> myClassFilter;

    /**
     * Instantiates a new Psi method dialog.
     *
     * @param project          the project
     * @param psiClass         the psi class
     * @param returnTypeFilter the return type filter
     * @param myScope          the my scope
     */
    public PsiMethodDialog(@Nullable Project project, @Nullable PsiClass psiClass, String returnTypeFilter, GlobalSearchScope myScope) {
        super(project);
        this.project = project;
        this.psiClass = psiClass;
        this.returnTypeFilter = returnTypeFilter;
        this.myScope = myScope;
        setModal(true);
        init();
        panel.setOpaque(false);
        myClassFilter = allFilter();
    }

    @Override
    public void doOKAction() {
        super.doOKAction();
    }

    @Override
    public void doCancelAction() {
        super.doCancelAction();
        this.selectMethod = null;
    }

    /**
     * Gets selected.
     *
     * @return the selected
     */
    public String getSelected() {
        return getPsiMethodInfo(selectMethod);
    }

    /**
     * Create choose by name model choose by name model.
     *
     * @return the choose by name model
     */
    protected ChooseByNameModel createChooseByNameModel() {
//        if (myBaseClass == null) {
        return new MyGotoClassModel<>(project, this);
//        }
//        else {
        //TODO shunyun 2024/1/15: 考虑继承自父类的方法

//             BaseClassInheritorsProvider<T> inheritorsProvider = getInheritorsProvider(myBaseClass);
//            if (inheritorsProvider != null) {
//                return new  SubclassGotoClassModel<>(myProject, this, inheritorsProvider);
//            }
//            else {
//                throw new IllegalStateException("inheritors provider is null");
//            }
//        }
    }

    private Set<Object> doFilter(Set<Object> elements) {
        Set<Object> result = new LinkedHashSet<>();
//        for (Object o : elements) {
//            if (myElementClass.isInstance(o) && allFilter().isAccepted(o)) {
//                result.add(o);
//            }
//        }
        return elements;
    }

    private TreeChooser.Filter<PsiMethod> allFilter() {
        return __ -> true;
    }

    private final class MyCallback extends ChooseByNamePopupComponent.Callback {
        @Override
        public void elementChosen(Object element) {
            selectMethod = (PsiMethod) element;
            close(OK_EXIT_CODE);
        }
    }

    private JPanel createPanel() {
        final JPanel dummyPanel = new JPanel(new BorderLayout());
        myGotoByNamePanel = new ChooseByNamePanel(project, createChooseByNameModel(), null, false, null) {
            @Override
            protected void showTextFieldPanel() {
            }

            @Override
            protected void close(boolean isOk) {
                super.close(isOk);
                if (isOk) {
                    doOKAction();
                } else {
                    doCancelAction();
                }
            }

            @Override
            protected @NotNull Set<Object> filter(@NotNull Set<Object> elements) {
                return doFilter(elements);
            }

            @Override
            protected void initUI(ChooseByNamePopupComponent.Callback callback, ModalityState modalityState, boolean allowMultipleSelection) {
                super.initUI(callback, modalityState, allowMultipleSelection);
                dummyPanel.add(myGotoByNamePanel.getPanel(), BorderLayout.CENTER);
                if (myProject != null && !myProject.isDefault() && DumbService.getInstance(myProject).isDumb()) {
                    JBLabel dumbLabel = new JBLabel(IdeBundle.message("dumb.mode.results.might.be.incomplete"), SwingConstants.LEFT);
                    dumbLabel.setIcon(AllIcons.General.Warning);
                    dumbLabel.setBorder(new JBEmptyBorder(10, 3, 0, 3));
                    dummyPanel.add(dumbLabel, BorderLayout.SOUTH);
                }
                IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance()
                        .requestFocus(IdeFocusTraversalPolicy.getPreferredFocusedComponent(myGotoByNamePanel.getPanel()), true));
            }

            @Override
            protected void showList() {
                super.showList();
                if (myInitialClass != null && myList.getModel().getSize() > 0) {
                    myList.setSelectedValue(myInitialClass, true);
                    myInitialClass = null;
                }
            }

            @Override
            protected void chosenElementMightChange() {
                handleSelectionChanged();
            }
        };

        Disposer.register(myDisposable, myGotoByNamePanel);

        myGotoByNamePanel.invoke(new MyCallback(), getModalityState(), false);

        return dummyPanel;
    }

    private void handleSelectionChanged() {
        selectMethod = calcSelectedClass();
        if (selectMethod == null) {
            setOKActionEnabled(false);
        } else {
            PsiMethod selectedClass = selectMethod;
            ReadAction
                    .nonBlocking(() -> DumbModeAccessType.RELIABLE_DATA_ONLY.ignoreDumbMode(() -> myClassFilter.isAccepted(selectedClass)))
                    .finishOnUiThread(getModalityState(), isAccepted -> setOKActionEnabled(selectMethod != null && isAccepted))
                    .submit(AppExecutorUtil.getAppExecutorService());
        }
    }

    private @NotNull ModalityState getModalityState() {
        return ModalityState.stateForComponent(getRootPane());
    }

    /**
     * Calc selected class psi method.
     *
     * @return the psi method
     */
    protected @Nullable PsiMethod calcSelectedClass() {
        return (PsiMethod) getGotoByNamePanel().getChosenElement();
    }

    /**
     * Gets goto by name panel.
     *
     * @return the goto by name panel
     */
    protected ChooseByNamePanel getGotoByNamePanel() {
        return myGotoByNamePanel;
    }

    @Nullable
    @Override
    protected JComponent createCenterPanel() {
        JPanel panel1 = createPanel();
        panel1.setMinimumSize(new Dimension(500, 400));
        panel1.setMaximumSize(new Dimension(500, 400));
        return panel1;
    }

    /**
     * Gets psi method info.
     *
     * @param psiMethod the psi method
     * @return the psi method info
     */
    protected String getPsiMethodInfo(PsiMethod psiMethod) {
        if (null == psiMethod) {
            return null;
        }
        Map<String, Object> result = new HashMap<>();
        result.put("className", psiClass.getQualifiedName());
        result.put("methodName", psiMethod.getName());
        result.put("returnType", getQualifiedName(psiMethod.getReturnType()));
        PsiParameterList parameterList = psiMethod.getParameterList();
        Map<String, String> params = new HashMap<>();
        for (PsiParameter parameter : parameterList.getParameters()) {
            params.put("paramName", parameter.getName());
            params.put("paramType", getQualifiedName(parameter.getType()));
        }
        result.put("params", List.of(params));

        result.put("returnEnums", PsiUtils.retrieveEnumValues(psiMethod.getReturnType()));

        return GsonUtils.toJson(result);
    }

    private String getQualifiedName(PsiType psiType) {
        if (psiType instanceof PsiClassReferenceType classReferenceType) {
            StringBuilder sb = new StringBuilder();
            PsiClass aClass = PsiTypesUtil.getPsiClass(psiType);
            if (aClass != null) {
                sb.append(aClass.getQualifiedName());
            } else {
                sb.append(psiType.getCanonicalText());
            }
            if (classReferenceType.hasParameters()) {
                sb.append("<");
                for (PsiType parameter : classReferenceType.getParameters()) {
                    sb.append(getQualifiedName(parameter));
                }
                sb.append(">");
            }
            return sb.toString();
        } else {
            PsiClass aClass = PsiTypesUtil.getPsiClass(psiType);
            if (aClass != null) {
                return aClass.getQualifiedName();
            } else {
                return psiType.getCanonicalText();
            }
        }
    }

    private PsiMethod[] getCanUseMethods(PsiClass psiClass) {
        PsiMethod[] all = psiClass.getAllMethods();
        if (all.length == 0) {
            return all;
        }

        Set<String> excludeSet = new HashSet<>();
        excludeSet.add("Object");
        excludeSet.add("registerNatives");
        excludeSet.add("getClass");
        excludeSet.add("hashCode");
        excludeSet.add("equals");
        excludeSet.add("clone");
        excludeSet.add("toString");
        excludeSet.add("notify");
        excludeSet.add("notifyAll");
        excludeSet.add("wait");
        excludeSet.add("finalize");

        List<PsiMethod> psiMethodList = new ArrayList<>();
        for (PsiMethod p : all) {
            if (StringUtils.isNotBlank(returnTypeFilter)) {
                PsiType returnPsiType = p.getReturnType();
                if (null == returnPsiType) {
                    continue;
                }

                String canonicalText = returnPsiType.getCanonicalText();
                if (StringUtils.isBlank(canonicalText)) {
                    continue;
                }
                if (returnTypeFilter.equals("enum")) {
                    if (!PsiUtils.isEnumValue(returnPsiType)) {
                        continue;
                    }
                } else {
                    if (!canonicalText.toLowerCase().equals(returnTypeFilter.toLowerCase())) {
                        continue;
                    }
                }
            }
            if (excludeSet.contains(p.getName())) {
                continue;
            }
            psiMethodList.add(p);
        }

        PsiMethod[] result = new PsiMethod[psiMethodList.size()];
        psiMethodList.toArray(result);
        return result;
    }

    /**
     * Gets scope.
     *
     * @return the scope
     */
    protected GlobalSearchScope getScope() {
        return myScope;
    }

    /**
     * Gets classes by name.
     *
     * @param name          the name
     * @param checkBoxState the check box state
     * @param pattern       the pattern
     * @param searchScope   the search scope
     * @return the classes by name
     */
    @NotNull
    protected List<PsiMethod> getClassesByName(final String name,
                                               final boolean checkBoxState,
                                               final String pattern,
                                               final GlobalSearchScope searchScope) {
        final PsiShortNamesCache cache = PsiShortNamesCache.getInstance(project);
//        PsiClass[] classes = DumbModeAccessType.RELIABLE_DATA_ONLY.ignoreDumbMode(() -> {
//            return cache
//                    .getClassesByName(name, checkBoxState ? searchScope : GlobalSearchScope.projectScope(project).intersectWith(searchScope));
//        });
        List<PsiMethod> methods = new ArrayList<>();
//        for (PsiClass aClass : classes) {
//            Collections.addAll(methods, aClass.getAllMethods());
//        }
        Collections.addAll(methods, getCanUseMethods(this.psiClass));
        return methods;
    }

    /**
     * Gets filter.
     *
     * @return the filter
     */
    protected @NotNull TreeChooser.Filter<PsiMethod> getFilter() {
        return myClassFilter;
    }

    /**
     * The type My goto class model.
     *
     * @param <T> the type parameter
     */
    protected static class MyGotoClassModel<T extends PsiNamedElement> extends GotoClassModel2 {
        private final PsiMethodDialog myTreeClassChooserDialog;

        /**
         * Instantiates a new My goto class model.
         *
         * @param project                the project
         * @param treeClassChooserDialog the tree class chooser dialog
         */
        public MyGotoClassModel(@NotNull Project project,
                                PsiMethodDialog treeClassChooserDialog) {
            super(project);
            myTreeClassChooserDialog = treeClassChooserDialog;
        }

        /**
         * Gets tree class chooser dialog.
         *
         * @return the tree class chooser dialog
         */
        PsiMethodDialog getTreeClassChooserDialog() {
            return myTreeClassChooserDialog;
        }

        @Override
        public Object @NotNull [] getElementsByName(@NotNull String name, @NotNull FindSymbolParameters parameters, @NotNull ProgressIndicator canceled) {
            String patternName = parameters.getLocalPatternName();
            List<PsiMethod> classes = myTreeClassChooserDialog.getClassesByName(
                    name, parameters.isSearchInLibraries(), patternName, myTreeClassChooserDialog.getScope()
            );
            if (classes.isEmpty()) return ArrayUtilRt.EMPTY_OBJECT_ARRAY;
            if (classes.size() == 1) {
                return isAccepted(classes.get(0)) ? ArrayUtil.toObjectArray(classes) : ArrayUtilRt.EMPTY_OBJECT_ARRAY;
            }
            Set<String> qNames = new HashSet<>();
            List<PsiMethod> list = new ArrayList<>(classes.size());
            for (PsiMethod aClass : classes) {
                if (qNames.add(getFullName(aClass)) && isAccepted(aClass)) {
                    list.add(aClass);
                }
            }
            return ArrayUtil.toObjectArray(list);
        }

        @Override
        public boolean isDumbAware() {
            return false;
        }

        @Override
        public @Nullable String getPromptText() {
            return null;
        }

        /**
         * Is accepted boolean.
         *
         * @param aClass the a class
         * @return the boolean
         */
        protected boolean isAccepted(PsiMethod aClass) {
            return myTreeClassChooserDialog.getFilter().isAccepted(aClass);
        }
    }
}
